Standalone single-deploy Modal packages for each stack - #12
Conversation
Repackage the Modal deployment so each self-hosted stack is a self-contained Modal App deployed with one `modal deploy` — no start_asr step, no modal.Dict address handshake, no cross-package dependency. Uses Modal Servers (@app.server) with sibling URLs resolved via Server.from_name at startup. - sync/modal_app.py -> aai-sync-u3pro - streaming/modal_app_universal_3_5_pro.py -> aai-streaming-u3pro - streaming/modal_app_english_multilang.py -> aai-streaming-english-multilang Fixes carried over from the #11 review: autoscaling ASR Server instead of a 24h-capped Sandbox; no 1h WebSocket session ceiling; encrypted ASR hop via h2_enabled + AAI_USE_SECURE_CHANNEL_TO_ASR_SERVICE (encrypted_ports alone negotiates no ALPN); unauthenticated=False by default (Modal proxy auth); fate-shared vendor processes; env-overridable sync audio limits. Verified end to end on L40S (single deploy each): sync 152-word transcript, autoscaled to 181x realtime at concurrency 8; streaming u3pro real turns over the TLS h2 ASR hop; english+multilang routing both models through the nginx Lb. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL3rQMpGXvmMHRJiffWMF1
sample_sync.py (POST /transcribe), sample_streaming.py (live realtime turns, model selection for the u3pro and english/multilang stacks), and a README. Both support --concurrency / --load for a quick input-load sweep and Modal proxy-auth headers. Verified live against all three deployed stacks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL3rQMpGXvmMHRJiffWMF1
The turn counter was appended only inside the live-printing branch, so --load sessions always reported 0 turns even though turns arrived (first-turn latency was correct). Track end_of_turn regardless of live; gate only printing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL3rQMpGXvmMHRJiffWMF1
…eaming_modal_stack/
Consolidate each stack's Modal artifacts into a self-contained top-level dir:
sync_modal_stack/ modal_app.py, sample_sync.py, README.md
streaming_modal_stack/ modal_app_universal_3_5_pro.py,
modal_app_english_multilang.py, sample_streaming.py, README.md
Each new dir carries its own deploy/verify/auth/teardown README. The compose
READMEs (sync/, streaming/) keep a one-line pointer instead of the full Modal
section, and the root README points at the two new dirs. No code behavior
changes; comment cross-references updated to the new paths.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LL3rQMpGXvmMHRJiffWMF1
The self-hosted-streaming-api v1.0.1 image carries the handshake-logging fix (DeepLearning #19523: peer-aborted WebSocket handshakes log at WARNING, not ERROR). Bump streaming-api and self-hosted-streaming-asr-universal-3-5-pro to release-v1.0.1 in the u3pro stack via per-image tags; the license-and-usage-proxy has no v1.0.1 and stays on v1.0.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL3rQMpGXvmMHRJiffWMF1
Fresh deploy + verification (v1.0.1 bump)Tore down all three apps and redeployed from scratch off this branch in the dlange Modal workspace (L40S), then verified:
Deploys used the reorganized layout ( Note: the english/multilang stack still uses |
Adversarial review of PR #12 (8 findings). Fixes: - [major] english/multilang ASR MAX_OPEN_STREAMS + target_concurrency 32 -> 48 (compose parity; avoids a ~50% GPU over-provision at scale). - [major] streaming README Verify pointed at the bundled example client, which cannot send Modal proxy-auth headers and 401s on the default deploy; point it at sample_streaming.py with --modal-key/--modal-secret and note the caveat. - [minor] sync/streaming README Verify curls now send Modal-Key/Modal-Secret (they 401'd on the default proxy-auth deploy). - [minor] nginx Lb -> ASR hop now verifies the backend cert (grpc_ssl_verify on + ca-certificates trusted store), not just encrypts. - [minor] fate-share reaper no longer reports a clean shutdown as a crash: a module-level _stopping event, set by @modal.exit stop(), distinguishes an intentional teardown from an unexpected vendor exit (all three apps). - [minor] sample_streaming.py no longer crashes formatting first_turn_s when a session yields no word-bearing turns (prints n/a). - [minor/question] english/multilang now pins streaming-api via its own API_TAG. streaming-api bumped to release-v1.0.1 everywhere it is referenced (u3pro + english/multilang Modal stacks, streaming/.env.example, root README) so both streaming stacks carry the handshake-logging fix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL3rQMpGXvmMHRJiffWMF1
…only for verification) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL3rQMpGXvmMHRJiffWMF1
…ectivity) Verifying the backend cert (finding 4) fails against Modal's edge: nginx cannot build the chain and errors "unable to get local issuer certificate", marking the ASR backend down so every session 3005s (confirmed live). Revert to encrypt-only (grpc_ssl_server_name on) and document the residual limitation in-place, the finding's sanctioned alternative. The backend is unauthenticated by design; co-location or i6pn is the real fix, per the README. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL3rQMpGXvmMHRJiffWMF1
Adversarial review fixes + streaming-api v1.0.1 everywhere — applied & verifiedRan an adversarial review (14 raw findings → 3 refuted → 8 confirmed) and addressed all of them:
[minor] LB→ASR cert verification ( Re-verified from scratch (dlange workspace, L40S): sync (26.9x realtime), u3pro v1.0.1 (12 turns), english + multilingual with the 48-cap and encrypt-only LB (8 / 14 turns). Handshake fix confirmed on both v1.0.1 streaming APIs — u3pro 42/42 and english/multilang 33/33 |
|
nice! From organization perspective can we add folders like streaming/modal streaming/docker streaming/sagemaker etc? I can see us adding more variations here and I dont think top level makes sense |
Per review feedback, nest deployment variants under each service dir instead of top-level *_modal_stack dirs, so more targets (e.g. sagemaker) slot in cleanly: sync/docker/ (compose stack, moved from sync/) sync/modal/ (moved from sync_modal_stack/) streaming/docker/ (compose stack + nginx, moved from streaming/) streaming/modal/ (moved from streaming_modal_stack/) Relative links and paths updated throughout (root README layout + pointers, docker READMEs' ../README.md -> ../../README.md and modal pointers, modal READMEs' example paths -> ../docker/example, sample-script docstrings, and the cross-file comment references). No code behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL3rQMpGXvmMHRJiffWMF1
|
Good call — done. Reorganized so each service groups its deployment targets, and the top-level I moved the existing compose stacks into |
bgotthold-aai
left a comment
There was a problem hiding this comment.
Approving this because I think it looks good overall.
Should we still add a .env for model to get the registry and tag out of code?
Update on the internal-hop exposure: i6pn is blocked on this plan (datacenter placement)Earlier in this thread I reported a CPU spike suggesting What I built. ASR + proxy as Why it can't be made reliable here (root-caused with a TCP reachability probe written to a Net. This is a Modal limitation for this deployment shape, not a code gap. #12 ships public-edge as evaluation-ready, with the front door authenticated (proxy auth on by default, verified 401 unauth / 200 authed) and the hops TLS-encrypted as the mitigation. Fully taking the internal services private would need Modal to enable same-datacenter placement, and I have the |
The internal-hop section presented co-location and i6pn as available fixes and called the topology evaluation-suitable without qualification. Reframe to match what the i6pn investigation found: the authenticated front door is the real gate, the residual backend exposure is bounded, and taking the backends fully private is a Modal placement limitation for this shape (GPU and CPU front-ends land in different datacenters; i6pn does not bridge them; same-datacenter placement is gated behind Modal sales). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL3rQMpGXvmMHRJiffWMF1
Runs each self-hosted stack on Modal's serverless GPUs. Each stack is a self-contained Modal App deployed with a single
modal deploy: the app defines its servers, and the API resolves its ASR and license-proxy URLs from the same App at startup, so one command brings up and wires the whole stack.Three self-contained packages
sync/modal/modal_app.pyaai-sync-u3proSyncApi(L40S),LicenseProxy(CPU)streaming/modal/modal_app_universal_3_5_pro.pyaai-streaming-u3proStreamingApi(CPU),Asr(L40S),LicenseProxy(CPU)streaming/modal/modal_app_english_multilang.pyaai-streaming-english-multilangStreamingApi(CPU),Lb(nginx),AsrEnglish(L40S),AsrMultilang(L40S),LicenseProxy(CPU)Each directory also ships a README and a sample client (
sample_sync.py/sample_streaming.py) that prints live output.Design
@app.server). The ASR runs as an autoscaling Server (min_containers=1,target_concurrencyset toMAX_OPEN_STREAMS), health-gated ongrpc_health_probe; WebSocket sessions are unbounded.modal.Server.from_name(...).get_url()resolves the ASR and proxy URLs from the same App at container start, so no address has to be wired in by hand.h2_enabled=Truemakes Modal's TLS edge advertise ALPN h2, so the API's default-TLS gRPC client connects withAAI_USE_SECURE_CHANNEL_TO_ASR_SERVICE=True. (encrypted_portsalone negotiates no ALPN and fails withmissing selected ALPN property;h2_ports/h2_enabledis required.)StreamingApi/SyncApishipunauthenticated=False(Modal proxy auth, enforced on the WebSocket upgrade);AAI_REQUIRE_MODAL_AUTH=0opts into a throwaway public test endpoint.MAX_AUDIO_DURATION_MS, etc.) read from the environment with the compose defaults as fallback.Lbthat routes thex-model-versiongRPC metadata (en-default/ml-default) to the two backends.streaming-apiand the Universal-3.5 Pro ASR are pinned torelease-v1.0.1(the API image logs peer-aborted handshakes at WARNING rather than ERROR); the license-and-usage-proxy stays onrelease-v1.0.0.Security note
The public front door (
StreamingApi/SyncApi) ships with Modal proxy auth on by default (unauthenticated=False), enforced at Modal's edge including the WebSocket upgrade. Verified live: an unauthenticatedPOST /transcribegets a 401 at the edge, an authenticated call returns a real 200 transcript. That is the real access gate.The residual exposure is the two internal services. The internal hops (
StreamingApito the ASR /Lb, and toLicenseProxy) cross Modal's public TLS edge because Modal has no private inter-container network by default. The hop is encrypted (TLS h2), but the backends and proxy rununauthenticated=Truesince the API dials them server-side and cannot attach Modal auth headers, so their.modal.directURLs are long and structured but public. That is obscurity, not access control. The risk is bounded: forging aPOST /v1/usageinflates the self-hoster's own usage bill (it does not grant free service), and reaching the ASR directly needs the URL and buys time on the operator's own GPU. No audio leaves the deployment and there is no cross-tenant surface.Taking the internal services fully private is a Modal limitation for this deployment shape, not a code gap. The two ways to do it both fall down here: co-locating everything in one container is not viable (three separate vendor images), and Modal's
i6pnprivate fabric only connects containers in the same underlying datacenter, which the credit-card plan does not guarantee (the L40S GPU ASR is placed in a different datacenter than the CPU front-ends, andi6pndoes not bridge datacenters; forcing same-datacenter placement is gated behind Modal sales). The full privatei6pnbuild was implemented and tested end to end to confirm this; it connects only when the containers happen to co-locate. See the exposure comment on this PR for the detail.Net: a gated front door plus encrypted hops is the right posture for evaluation and POC. Closing the internal exposure for a stricter production bar needs Modal to enable same-datacenter placement.
Validated live (Modal, L40S)
Deployed from scratch and torn down:
universal-streaming-englishanduniversal-streaming-multilingualreturn real turns, each routed by the nginxLbto its own L40S backend.Proxy is connected and license is validand SIGKILL on failure; an invalid license makes the proxy exit 1 at startup.usage-tracker.assemblyai.com/v1/usagewithstatus_code=200.opening handshake failed(from L4 health probes and aborted clients) logs at WARNING, not ERROR.🤖 Generated with Claude Code